#include	"Font.h"
#include	"FontException.h"
#include	<Textures/ImageTGA.h>
#include	<OpenGL/GLException.h>
#include	<iostream>
#include	<fstream>
#include	<sstream>
#include	<string>
#include	<cassert>
#include	<windows.h>
#include	<gl/gl.h>

Font::Font()
	:	m_fileName("")
	,	m_info()
	,	m_common()
	,	m_page()
	,	m_symbol()
	,	m_kerning()
	,	m_texture()
{
}

Font::Font(const std::string & fileName)
	:	m_fileName("")
	,	m_info()
	,	m_common()
	,	m_page()
	,	m_symbol()
	,	m_kerning()
	,	m_texture()
{
	Load(fileName);
}

Font::~Font()
{
	Cleanup();
}

void Font::Load(const std::string & fileName)
{
	m_fileName = fileName;
	LoadBMFFile();
	FillMissingChars();
	LoadTextures();
}

void Font::Cleanup()
{
	CleanupTextures();
	CleanupBMFData();
	m_fileName = "";
}

void Font::Print(const Vector2f & pos, unsigned int size,
	const std::string & text, const Color & color,
	HorizontalPosition horizontalPosition, VerticalPosition verticalPosition) const
{
	glPushAttrib(GL_CURRENT_BIT  | GL_ENABLE_BIT | GL_TRANSFORM_BIT);	
		glEnable(GL_DEPTH_TEST);
		glEnable(GL_CULL_FACE);
		glEnable(GL_TEXTURE_2D);
		glEnable(GL_COLOR_MATERIAL);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glColor4fv(color);
		unsigned int lineNum = 0;
		std::string::size_type pnl = 0, nl = 0;
		Vector2f position = pos;
		float height = TextHeight(size, text);
		float lineHeight = TextHeight(size, " ");
		switch(verticalPosition)
		{
		case vpTop:
			break;
		case vpCenter:
			position[1] += height / 2;
			break;
		case vpBottom:
			position[1] += height;
			break;
		}
		position[1] -= 3 * lineHeight / 4;
		std::string sub;
		for(nl = text.find_first_of('\n'), pnl = 0;
			nl != std::string::npos; pnl = nl + 1, nl = text.find_first_of('\n', pnl), ++lineNum)
		{
			sub = text.substr(pnl, nl - pnl);
			float lineWidth = TextWidth(size, sub);
			switch(horizontalPosition)
			{
			case hpLeft:
				position[0] = pos[0];
				break;
			case hpCenter:
				position[0] = pos[0] - lineWidth / 2;
				break;
			case hpRight:
				position[0] = pos[0] - lineWidth;
				break;
			}
			PrintLine(Vector2f(position[0], position[1] - lineHeight * lineNum), size, sub);
		}
		sub = text.substr(pnl);
		float lineWidth = TextWidth(size, sub);
		switch(horizontalPosition)
		{
		case hpLeft:
			position[0] = pos[0];
			break;
		case hpCenter:
			position[0] = pos[0] - lineWidth / 2;
			break;
		case hpRight:
			position[0] = pos[0] - lineWidth;
			break;
		}
		PrintLine(Vector2f(position[0], position[1] - lineHeight * lineNum), size, sub);
	glPopAttrib();
}

float Font::TextHeight(unsigned int size, const std::string & text) const
{
	unsigned int lineNum = 0;
	float scaleFactor = float(size) / float(m_common.lineHeight);
	for(std::string::size_type nl = text.find_first_of('\n');
		nl != std::string::npos; nl = text.find_first_of('\n', nl + 1))
	{
		++lineNum;
	}
	++lineNum;
	return float(lineNum * m_common.lineHeight) * scaleFactor;
}

float Font::TextWidth(unsigned int size, const std::string & text) const
{
	float maxWidth = 0.0f, width;
	std::string::size_type pnl = 0, nl = 0;
	for(nl = text.find_first_of('\n'), pnl = 0;
		nl != std::string::npos; pnl = nl + 1, nl = text.find_first_of('\n', pnl))
	{
		width = LineWidth(size, text.substr(pnl, nl - pnl));
		if(maxWidth < width)
		{
			maxWidth = width;
		}
	}
	width = LineWidth(size, text.substr(pnl));
	if(maxWidth < width)
	{
		maxWidth = width;
	}
	return maxWidth;
}

Vector2f Font::TextSize(unsigned int size, const std::string & text) const
{
	return Vector2f(TextWidth(size, text), TextHeight(size, text));
}

std::string Font::GetFontDir() const
{
	std::string::size_type lastSlash = m_fileName.find_last_of("\\/");
	if(lastSlash == std::string::npos)
	{
		lastSlash = 0;
	}
	else
	{
		lastSlash += 1;
	}
	return m_fileName.substr(0, lastSlash);
}

void Font::LoadBMFFile()
{
	std::ifstream bmf(m_fileName.c_str());
	if(!bmf)
	{
		throw FontException("Couldn\'t open the font file. Check if the path is correct.");
	}
	std::string line;
	// read info line
	std::getline(bmf, line);
	if(bmf)
	{
		LoadInfo(line);
	}
	// read common line
	std::getline(bmf, line);
	if(bmf)
	{
		LoadCommon(line);
	}
	// create pages
	m_page.resize(m_common.pageCount);
	// read every page
	for(unsigned int i = 0; i < m_common.pageCount; ++i)
	{
		std::getline(bmf, line);
		if(bmf)
		{
			LoadPage(line);
		}
	}
	// read number of symbols
	std::getline(bmf, line);
	unsigned int symbolCount = 0;
	if(bmf)
	{
		symbolCount = LoadChars(line);
	}
	// create symbols
	if(m_info.unicode)
	{
		m_symbol.resize(0x00010000);// unicode font = 2^16 characters
	}
	else
	{
		m_symbol.resize(0x00000100);// ascii font = 2^8 characters
	}
	// read every symbol
	for(unsigned int i = 0; i < symbolCount; ++i)
	{
		std::getline(bmf, line);
		if(bmf)
		{
			LoadChar(line);
		}
	}
	// read number of kernings
	std::getline(bmf, line);
	unsigned int kernCount = 0;
	if(bmf)
	{
		kernCount = LoadKernings(line);
	}
	// create kernings
	m_kerning.resize(kernCount);
	// read every kerning
	for(unsigned int i = 0; i < kernCount; ++i)
	{
		std::getline(bmf, line);
		if(bmf)
		{
			LoadKerning(line, i);
		}
	}
	bmf.close();
}

void Font::CleanupBMFData()
{
	m_kerning.clear();
	m_symbol.clear();
	m_page.clear();
	std::memset(&m_common, 0, sizeof(Common));
	std::memset(&m_info, 0, sizeof(Info));
}

std::pair< std::string, std::string > Font::GetNextPair(const std::string & data, std::string::size_type & off)
{
	std::pair< std::string, std::string > result("", "");
	std::string::size_type start = data.find_first_not_of(' ', off);
	std::string::size_type eq = data.find_first_of('=', start);
	std::string::size_type end = std::string::npos;
	if((start != std::string::npos) && (eq != std::string::npos))
	{
		if(data.at(eq + 1) != '\"')
		{
			end = data.find_first_of(' ', eq);
		}
		else
		{
			end = data.find_first_of('\"', eq + 2);
			while((end != std::string::npos) && (data.at(end - 1) == '\\'))
			{
				end = data.find_first_of('\"', end + 1);
			}
			if(end != std::string::npos)
			{
				end += 1;
			}
		}
		result.first = data.substr(start, eq - start);
		if(end != std::string::npos)
		{
			result.second = data.substr(eq + 1, end - eq - 1);
		}
		else
		{
			result.second = data.substr(eq + 1);
		}
	}
	off = end;
	return result;
}

void Font::LoadInfo(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "info");
	std::string::size_type off = 4;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	while(pair.first != "")
	{
		stream.clear();
		if(pair.first == "face")
		{
			m_info.name = pair.second.substr(1, pair.second.length() - 2);
		}
		else if(pair.first == "size")
		{
			stream.str(pair.second);
			stream >> m_info.size;
		}
		else if(pair.first == "bold")
		{
			m_info.bold = (pair.second == "1");
		}
		else if(pair.first == "italic")
		{
			m_info.italic = (pair.second == "1");
		}
		else if(pair.first == "charset")
		{
			m_info.charset = pair.second.substr(1, pair.second.length() - 2);
		}
		else if(pair.first == "unicode")
		{
			m_info.unicode = (pair.second == "1");
		}
		else if(pair.first == "stretchH")
		{
			stream.str(pair.second);
			int tmp = 100;
			stream >> tmp;
			assert(tmp <= 100);
			m_info.heightStretch = static_cast< unsigned char >(tmp);
		}
		else if(pair.first == "smooth")
		{
			m_info.smooth = (pair.second == "1");
		}
		else if(pair.first == "aa")
		{
			stream.str(pair.second);
			int tmp = 1;
			stream >> tmp;
			m_info.superSamplingLevel = static_cast< unsigned char >(tmp);
		}
		else if(pair.first == "padding")
		{
			stream.str(pair.second);
			stream >> m_info.padding[0];
			stream.ignore();
			stream >> m_info.padding[1];
			stream.ignore();
			stream >> m_info.padding[2];
			stream.ignore();
			stream >> m_info.padding[3];
		}
		else if(pair.first == "spacing")
		{
			stream.str(pair.second);
			stream >> m_info.spacing[0];
			stream.ignore();
			stream >> m_info.spacing[1];
		}
		else
		{
			throw FontException("Unknown token found while loading info parameters");
		}
		pair = GetNextPair(line, off);
	}
}

void Font::LoadCommon(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "common");
	std::string::size_type off = 6;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	while(pair.first != "")
	{
		stream.clear();
		if(pair.first == "lineHeight")
		{
			stream.str(pair.second);
			stream >> m_common.lineHeight;
		}
		else if(pair.first == "base")
		{
			stream.str(pair.second);
			stream >> m_common.lineBase;
		}
		else if(pair.first == "scaleW")
		{
			stream.str(pair.second);
			stream >> m_common.scale[0];
		}
		else if(pair.first == "scaleH")
		{
			stream.str(pair.second);
			stream >> m_common.scale[1];
		}
		else if(pair.first == "pages")
		{
			stream.str(pair.second);
			stream >> m_common.pageCount;
		}
		else if(pair.first == "packed")
		{
			m_common.packed = (pair.second == "1");
		}
		else
		{
			throw FontException("Unknown token found while loading common parameters");
		}
		pair = GetNextPair(line, off);
	}
}

void Font::LoadPage(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "page");
	std::string::size_type off = 4;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	unsigned int id = 0xFFFFFFFF;
	while(pair.first != "")
	{
		stream.clear();
		if(pair.first == "id")
		{
			stream.str(pair.second);
			stream >> id;
			m_page[id].id = id;
		}
		else if(pair.first == "file")
		{
			assert(id != 0xFFFFFFFF);
			m_page[id].fileName = pair.second.substr(1, pair.second.length() - 2);
		}
		else
		{
			throw FontException("Unknown token found while loading page parameters");
		}
		pair = GetNextPair(line, off);
	}
}

unsigned int Font::LoadChars(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "chars");
	std::string::size_type off = 5;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	unsigned int result;
	stream.clear();
	if(pair.first == "count")
	{
		stream.str(pair.second);
		stream >> result;
	}
	else
	{
		throw FontException("Unknown token found while loading chars parameters");
	}
	return result;
}

void Font::LoadChar(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "char");
	std::string::size_type off = 4;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	unsigned int id = 0xFFFFFFFF;
	while(pair.first != "")
	{
		stream.clear();
		if(pair.first == "id")
		{
			stream.str(pair.second);
			stream >> id;
			assert(id < m_symbol.size());
			m_symbol[id].id = id;
		}
		else if(pair.first == "x")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].texturePosition[0];
		}
		else if(pair.first == "y")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].texturePosition[1];
		}
		else if(pair.first == "width")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].textureDimension[0];
		}
		else if(pair.first == "height")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].textureDimension[1];
		}
		else if(pair.first == "xoffset")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].drawOffset[0];
		}
		else if(pair.first == "yoffset")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].drawOffset[1];
		}
		else if(pair.first == "xadvance")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].advance;
		}
		else if(pair.first == "page")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].page;
		}
		else if(pair.first == "chnl")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_symbol[id].channel;
		}
		else
		{
			throw FontException("Unknown token found while loading char parameters");
		}
		pair = GetNextPair(line, off);
	}
}

unsigned int Font::LoadKernings(const std::string & line)
{
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "kernings");
	std::string::size_type off = 8;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	unsigned int result;
	stream.clear();
	if(pair.first == "count")
	{
		stream.str(pair.second);
		stream >> result;
	}
	else
	{
		throw FontException("Unknown token found while loading kernings parameters");
	}
	return result;
}

void Font::LoadKerning(const std::string & line, unsigned int id)
{
	assert(id < m_kerning.size());
	std::istringstream stream(line);
	std::string firstWord;
	stream >> firstWord;
	assert(firstWord == "kerning");
	std::string::size_type off = 7;
	std::pair< std::string, std::string > pair = GetNextPair(line, off);
	while(pair.first != "")
	{
		stream.clear();
		if(pair.first == "first")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_kerning[id].left;
		}
		else if(pair.first == "second")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_kerning[id].right;
		}
		else if(pair.first == "amount")
		{
			assert(id != 0xFFFFFFFF);
			stream.str(pair.second);
			stream >> m_kerning[id].amount;
		}
		else
		{
			throw FontException("Unknown token found while loading kerning parameters");
		}
		pair = GetNextPair(line, off);
	}
}

void Font::FillMissingChars()
{
	Symbol space = m_symbol[' '];
	unsigned int id = 0;
	for(std::vector< Symbol >::iterator it = m_symbol.begin(); it != m_symbol.end(); ++it, ++id)
	{
		if((*it).id != id)
		{
			(*it) = space;
			(*it).id = id;
		}
	}
}

void Font::LoadTextures()
{
	m_texture.resize(m_page.size());
	ImageTGA image;
	std::string dir = GetFontDir();
	for(unsigned int i = 0; i < m_page.size(); ++i)
	{
		std::string name = dir + m_page[i].fileName;
		image.Load(name.c_str());
		m_texture[i] = new Texture(image);
		image.Cleanup();
	}
}

void Font::CleanupTextures()
{
	for(unsigned int i = 0; i < m_texture.size(); ++i)
	{
		delete m_texture[i];
		m_texture[i] = 0;
	}
	m_texture.clear();
}

float Font::RenderSymbol(unsigned int id, float advance) const
{
	if((m_symbol[id].textureDimension[0] != 0) && (m_symbol[id].textureDimension[1] != 0))
	{
		Symbol s = m_symbol[id];
		Vector2f texPos = s.texturePosition;
		Vector2f texDim = s.textureDimension;
		Vector2f scale (1.0f / float(m_common.scale[0]), 1.0f / float(m_common.scale[1]));
		Vector2f offset = s.drawOffset;
		glTexCoord2f(texPos[0] * scale[0], 1.0f - texPos[1] * scale[1]);
		glVertex2f(advance + offset[0], m_common.lineBase - offset[1]);
		glTexCoord2f(texPos[0] * scale[0], 1.0f - (texPos[1] + texDim[1]) * scale[1]);
		glVertex2f(advance + offset[0], m_common.lineBase - (offset[1] + texDim[1]));
		glTexCoord2f((texPos[0] + texDim[0]) * scale[0], 1.0f - (texPos[1] + texDim[1]) * scale[1]);
		glVertex2f(advance + offset[0] + texDim[0], m_common.lineBase - (offset[1] + texDim[1]));
		glTexCoord2f((texPos[0] + texDim[0]) * scale[0], 1.0f - texPos[1] * scale[1]);
		glVertex2f(advance + offset[0] + texDim[0], m_common.lineBase - offset[1]);
	}
	return float(m_symbol[id].advance);
}

void Font::PrintLine(const Vector2f & pos, unsigned int size, const std::string & line) const
{
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
		float scaleFactor = float(size) / float(m_common.lineHeight);
		float advance = 0.0f;
		glTranslatef(pos[0], pos[1], 0.0f);
		glScalef(scaleFactor, scaleFactor, 1.0f);
		unsigned int currentPage = unsigned int(-1);
		for(unsigned int i = 0; i < line.length(); ++i)
		{
			unsigned int c = unsigned char(line[i]);
			if(currentPage != m_symbol[c].page)
			{
				if(currentPage != unsigned int(-1))
				{
					glEnd();
				}
				currentPage = m_symbol[c].page;
				glBindTexture(GL_TEXTURE_2D, m_texture[currentPage]->Name());
				glBegin(GL_QUADS);
			}
			advance += RenderSymbol(c, advance);
		}
		if(currentPage != unsigned int(-1))
		{
			glEnd();
		}
	glPopMatrix();
}

float Font::LineWidth(unsigned int size, const std::string & line) const
{
	float width = 0.0f;
	float scaleFactor = float(size) / float(m_common.lineHeight);
	for(unsigned int i = 0; i < line.length(); ++i)
	{
		unsigned int c = unsigned char(line[i]);
		width += float(m_symbol[c].advance) * scaleFactor;
	}
	return width;
}

